CrewSessionTab.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. 'use client';
  2. import { useState, useEffect, useCallback } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import type { ActiveSessionResponse, SessionHistoryResponse, SessionHistoryItem } from '@/types/response/crew/session';
  5. import { Button } from '@/components/ui/button';
  6. import { Input } from '@/components/ui/input';
  7. type Props = { crewID: number };
  8. export default function CrewSessionTab({ crewID }: Props)
  9. {
  10. const [activeSession, setActiveSession] = useState<ActiveSessionResponse>(null);
  11. const [history, setHistory] = useState<SessionHistoryItem[]>([]);
  12. const [historyTotal, setHistoryTotal] = useState(0);
  13. const [loading, setLoading] = useState(true);
  14. const [sessionTitle, setSessionTitle] = useState('');
  15. const [starting, setStarting] = useState(false);
  16. const [ending, setEnding] = useState(false);
  17. const fetchActiveSession = useCallback(async () => {
  18. try {
  19. const res = await fetchApi<ActiveSessionResponse>(`/api/studio/crew/session/active/${crewID}`);
  20. setActiveSession(res.data ?? null);
  21. } catch {}
  22. }, [crewID]);
  23. const fetchHistory = useCallback(async () => {
  24. try {
  25. const res = await fetchApi<SessionHistoryResponse>(`/api/studio/crew/session/history/${crewID}?page=1&perPage=10`);
  26. setHistory(res.data?.list ?? []);
  27. setHistoryTotal(res.data?.total ?? 0);
  28. } catch {}
  29. }, [crewID]);
  30. useEffect(() => {
  31. setLoading(true);
  32. Promise.all([fetchActiveSession(), fetchHistory()]).finally(() => setLoading(false));
  33. }, [fetchActiveSession, fetchHistory]);
  34. const handleStart = async () => {
  35. if (!sessionTitle.trim()) { alert('방송 제목을 입력해 주세요.'); return; }
  36. setStarting(true);
  37. try {
  38. await fetchApi('/api/crew/session/start', { method: 'POST', body: { crewID, title: sessionTitle.trim() } });
  39. setSessionTitle('');
  40. fetchActiveSession();
  41. } catch (err: unknown) {
  42. alert(err instanceof Error ? err.message : '세션 시작에 실패했습니다.');
  43. } finally { setStarting(false); }
  44. };
  45. const handleEnd = async () => {
  46. if (!activeSession) return;
  47. if (!confirm('크루 방송을 종료하시겠습니까?\n종료 시 크루원에게 정산 결과가 전송됩니다.')) return;
  48. setEnding(true);
  49. try {
  50. await fetchApi('/api/crew/session/end', { method: 'POST', body: { crewSessionID: activeSession.crewSessionID } });
  51. setActiveSession(null);
  52. fetchHistory();
  53. } catch (err: unknown) {
  54. alert(err instanceof Error ? err.message : '세션 종료에 실패했습니다.');
  55. } finally { setEnding(false); }
  56. };
  57. if (loading) return <p className="studio-page__empty">준비 중...</p>;
  58. return (
  59. <>
  60. {activeSession ? (
  61. <div className="session-active">
  62. <div className="session-active__card">
  63. <div className="session-active__top">
  64. <div className="session-active__info">
  65. <span className="session-active__session-title">{activeSession.title}</span>
  66. <span className={`studio-page__badge studio-page__badge--${activeSession.status === 'Active' ? 'active' : 'warning'}`}>
  67. {activeSession.status === 'Inviting' ? '동의 대기 중' : '방송 중'}
  68. </span>
  69. </div>
  70. <Button variant="destructive" size="sm" onClick={handleEnd} disabled={ending || activeSession.status === 'Inviting'}>
  71. {ending ? '종료 중...' : '방송 종료'}
  72. </Button>
  73. </div>
  74. <div className="session-active__stats">
  75. <div className="session-active__stat"><div className="session-active__stat-value">{activeSession.totalAmount.toLocaleString()}원</div><div className="session-active__stat-label">총 후원액</div></div>
  76. <div className="session-active__stat"><div className="session-active__stat-value">{activeSession.totalDonationCount}건</div><div className="session-active__stat-label">후원 건수</div></div>
  77. <div className="session-active__stat"><div className="session-active__stat-value">{activeSession.consents.filter(c => c.isConsented).length}/{activeSession.consents.length}</div><div className="session-active__stat-label">동의 현황</div></div>
  78. </div>
  79. <div className="session-consents">
  80. <div className="session-consents__title">크루원 동의 현황</div>
  81. <div className="session-consents__list">
  82. {activeSession.consents.map(c => (
  83. <div key={c.crewMemberID} className={`session-consents__item session-consents__item--${c.isConsented ? 'consented' : 'pending'}`}>
  84. <span className="session-consents__icon">{c.isConsented ? '✓' : '⏳'}</span>{c.nickname}
  85. </div>
  86. ))}
  87. </div>
  88. </div>
  89. {activeSession.status === 'Active' && activeSession.summaries.length > 0 && (
  90. <div style={{ marginTop: 16 }}>
  91. <table className="studio-page__table">
  92. <thead><tr><th>순위</th><th>크루원</th><th>후원액</th><th>건수</th><th>기여율</th></tr></thead>
  93. <tbody>
  94. {activeSession.summaries.map(s => (
  95. <tr key={s.crewMemberID}><td>{s.rank}위</td><td>{s.nickname}</td><td>{s.totalAmount.toLocaleString()}원</td><td>{s.donationCount}건</td><td>{s.contributionRate.toFixed(1)}%</td></tr>
  96. ))}
  97. </tbody>
  98. </table>
  99. </div>
  100. )}
  101. </div>
  102. </div>
  103. ) : (
  104. <div className="session-start">
  105. <div className="session-start__title">새 크루 방송 시작</div>
  106. <div className="session-start__form">
  107. <Input className="session-start__input" placeholder="방송 제목을 입력하세요" value={sessionTitle} onChange={e => setSessionTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleStart()} />
  108. <Button onClick={handleStart} disabled={starting}>{starting ? '시작 중...' : '방송 시작'}</Button>
  109. </div>
  110. </div>
  111. )}
  112. <div className="session-history">
  113. <div className="session-history__title">지난 방송 ({historyTotal}건)</div>
  114. <div className="studio-page__table-wrap">
  115. <table className="studio-page__table">
  116. <thead><tr><th>제목</th><th>총 후원액</th><th>건수</th><th>시작</th><th>종료</th></tr></thead>
  117. <tbody>
  118. {history.length === 0 ? (
  119. <tr><td colSpan={5} className="studio-page__empty">아직 진행한 크루 방송이 없습니다.</td></tr>
  120. ) : history.map(h => (
  121. <tr key={h.id}><td>{h.title}</td><td>{h.totalAmount.toLocaleString()}원</td><td>{h.totalDonationCount}건</td><td>{h.startedAt ? new Date(h.startedAt).toLocaleString('ko-KR') : '-'}</td><td>{h.endedAt ? new Date(h.endedAt).toLocaleString('ko-KR') : '-'}</td></tr>
  122. ))}
  123. </tbody>
  124. </table>
  125. </div>
  126. </div>
  127. </>
  128. );
  129. }